Description
Show league tables/standings (W-L, GP, pts, GB/GD, rank) for the selected league, fetched from ESPN's standings endpoint. A new view reachable from the dashboard; respects the user's enabled-league set. Soccer = group/table standings; stick-and-ball = division/conference records.
Acceptance Criteria
- #1 User can open a standings view for a single league
- #2 Standings show rank, record, and league-appropriate columns (pts/GB/GD)
- #3 View uses the ESPN standings endpoint and the existing model/espn/ui layering
- #4 Only enabled leagues (App.leagues) are selectable
Implementation Plan
- model.Standings{League,Groups[]} -> StandingsGroup{Name,Columns[],Rows[]} -> StandingsRow{Rank,Team,Values[]}.
- espn.Client.Standings(ctx,l): GET apis/v2 base (not apis/site/v2); recursive walk of children -> emit a group per node that has entries (soccer=groups, MLB=AL/NL, NBA/NHL/NFL=conferences). Per-sport column spec (soccer P/W/D/L/GD/Pts; baseball+nba PCT/GB; nfl T; nhl OTL/PTS). Stats keyed by type->displayValue. Rank = entry index.
- UI viewStandings: open with key from dashboard for selected league (filter=All -> first enabled; tab/arrows cycle leagues). Own cursor over flattened team rows; enter opens that team's schedule (TASK-017). esc closes. Grouped, ranked, team-colored tables; scroll-clamped body like detail.
Final Summary
Added a standings view (viewStandings, opened with 'S').
What:
- model.Standings (Groups -> StandingsGroup{Name,Columns,Rows}) with RowCount/RowAt flattening helpers for cursor navigation.
- espn.Client.Standings fetches from ESPN's standings host (apis/v2/sports, NOT the scoreboard's apis/site/v2). mapStandings recursively walks ESPN's nested league/conference tree, emitting one group per table that has entries (soccer=12 groups, MLB=AL/NL, NBA/NHL/NFL=conferences). Per-sport column spec (soccer P/W/D/L/GD/Pts; baseball/nba W/L/PCT/GB; nfl adds T; nhl OTL/PTS); stats pulled by ESPN type. Rank = entry order.
- UI: standings.go renders grouped, ranked, team-colored tables with a movable team-row cursor and scroll-follow. Open from the dashboard for the active league (all-leagues -> first enabled); tab/shift+tab cycle leagues, r refreshes, enter opens the selected team's schedule (TASK-017), esc closes. Only enabled leagues (App.leagues) are selectable.
Tests: mapStandings (nested flatten + per-sport columns + missing-stat blanks), Standings.RowAt/RowCount. Live-smoke verified MLB AL/NL and World Cup 12 groups map correctly. go vet + go test ./... pass.